package puzzle.common.view.swing;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Font;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetAdapter;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Writer;
import java.util.Date;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.border.BevelBorder;
import javax.swing.border.Border;

import puzzle.common.data.AbstractBoard;
import puzzle.common.loader.AbstractLoader;
import tomoto.assertion.AssertUtil;

@SuppressWarnings("serial")
public class PuzzleSolverAppPanel<B extends AbstractBoard<B>> extends JPanel {

	public static String IS_BOARD_READ = "isBoardRead";
	public static String IS_AVAILABLE = "isAvailable";

	private Border m_emptyBorder;
	@SuppressWarnings("unused")
	private Border m_statusBarBorder;
	@SuppressWarnings("unused")
	private DropTarget m_dropTarget;

	private JPanel m_statusBar;
	private JLabel m_statusMessageLabel;
	private JScrollPane m_mainPanel;
	private AbstractPuzzleViewPanel<B> m_puzzleViewPanel;
	private PuzzleSolverController<B> m_controller;
	private B m_initialBoard;

	private PuzzleSolverAppMetadata<B> m_metadata;

	public PuzzleSolverAppPanel(PuzzleSolverAppMetadata<B> metadata) {
		m_metadata = metadata;
		initComponents();
	}

	//
	// Initialize components
	//

	private void initComponents() {
		m_emptyBorder = BorderFactory.createEmptyBorder(2, 2, 2, 2);

		createMainPanel();
		createStatusBar();

		setLayout(new BorderLayout());
		add(m_mainPanel, BorderLayout.CENTER);
		add(m_statusBar, BorderLayout.SOUTH);

		m_dropTarget = new DropTarget(m_puzzleViewPanel, new DropTargetAdapter() {
			@Override
			public void dragEnter(DropTargetDragEvent dtde) {
				dropTarget_dragEnter(dtde);
			}

			public void drop(DropTargetDropEvent dtde) {
				dropTarget_drop(dtde);
			}

		});
	}

	private void createStatusBar() {
		m_statusBar = new JPanel();
		m_statusBar.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));
		m_statusBar.setLayout(new BorderLayout());

		m_statusMessageLabel = new JLabel("Status message comes here.", JLabel.LEFT);
		m_statusMessageLabel.setBorder(m_emptyBorder);
		m_statusBar.add(m_statusMessageLabel);
	}

	private void createMainPanel() {
		m_puzzleViewPanel = m_metadata.createPuzzleViewPanel();
		m_puzzleViewPanel.setFont(Font.decode("arial-bold-24"));
		m_mainPanel = new JScrollPane(m_puzzleViewPanel);

		m_controller = new PuzzleSolverController<B>(m_metadata);
		m_controller.setPuzzleViewPanel(m_puzzleViewPanel);
		m_controller.setStatusWriter(new Writer() {
			public void close() {
			}

			public void flush() {
			}

			public void write(char[] cbuf, int off, int len) {
				m_statusMessageLabel.setText(new String(cbuf, off, len));
			}
		});
		m_controller.addPropertyChangeListener(PuzzleSolverController.IS_SOLVER_THREAD_ACTIVE,
				new PropertyChangeListener() {
					public void propertyChange(PropertyChangeEvent evt) {
						boolean newValue = !(((Boolean) evt.getNewValue()).booleanValue());
						firePropertyChange(IS_AVAILABLE, !newValue, newValue);
					}
				});
	}

	//
	// event handlers
	//

	private void dropTarget_dragEnter(DropTargetDragEvent dtde) {
		if (!isAvailable()) {
			dtde.rejectDrag();
			return;
		}

		if (!dtde.getCurrentDataFlavorsAsList().contains(DataFlavor.javaFileListFlavor)) {
			dtde.rejectDrag();
			return;
		}

	}

	private void dropTarget_drop(DropTargetDropEvent dtde) {
		if (!isAvailable()) {
			dtde.rejectDrop();
			return;
		}

		if (!dtde.getCurrentDataFlavorsAsList().contains(DataFlavor.javaFileListFlavor)) {
			dtde.rejectDrop();
			return;
		}

		dtde.acceptDrop(dtde.getDropAction());

		List<File> files;
		try {
			files = getFilesFromDropEvent(dtde);
			dtde.dropComplete(true);
		} catch (IOException ex) {
			ex.printStackTrace();
			dtde.dropComplete(false);
			return;
		}

		openFile(files.get(0));
	}

	@SuppressWarnings("unchecked")
	private List<File> getFilesFromDropEvent(DropTargetDropEvent dtde) throws IOException {
		try {
			return (List<File>) dtde.getTransferable().getTransferData(
					DataFlavor.javaFileListFlavor);
		} catch (UnsupportedFlavorException ex) {
			throw AssertUtil.fail();
		}
	}

	//
	// Actions
	//

	public void openFile(File file) {
		try {
			Reader r = new InputStreamReader(new FileInputStream(file));
			openReader(r);
		} catch (IOException ex) {
			setStatusMessage("Failed to open file: " + ex);
			ex.printStackTrace();
		}
	}

	public void openReader(Reader r) {
		B prevInitialBoard = m_initialBoard;

		try {
			AbstractLoader<B> l = m_metadata.createLoader();
			m_initialBoard = l.load(r);
		} catch (IOException ex) {
			setStatusMessage("Failed to read the problem data." + ex);
			ex.printStackTrace();
			return;
		}

		if (prevInitialBoard == null && m_initialBoard != null) {
			firePropertyChange(IS_BOARD_READ, false, true);
		}

		m_puzzleViewPanel.setBoard(m_initialBoard);
		m_puzzleViewPanel.adjustFontSize();
		repackFrame();
		startSolver();
	}

	public void startSolver() {
		if (m_initialBoard == null) {
			setStatusMessage("Problem data is not yet read.");
			return;
		}

		B b = m_initialBoard.dup();
		m_puzzleViewPanel.setBoard(b);
		m_controller.startSolver(b);
	}

	public void stopSolver() {
		m_controller.interrupt();
	}

	public void togglePauseSolver() {
		if (m_controller.isSuspensionRequested()) {
			setStatusMessage("RESUME at " + new Date());
			m_controller.resume();
		} else {
			setStatusMessage("PAUSE at " + new Date());
			m_controller.suspend();
		}
	}

	//
	// helper logics
	//

	private void repackFrame() {
		Container c = this;
		while (c != null && !(c instanceof JFrame)) {
			c = c.getParent();
		}
		((JFrame) c).pack();
	}

	public void setStatusMessage(String text) {
		m_statusMessageLabel.setText(text);
	}

	//
	// Properties
	//

	public boolean isAvailable() {
		return !m_controller.isSolverThreadActive();
	}

	public boolean isBoardRead() {
		return m_initialBoard != null;
	}

	public void setSpeed(int speed) {
		m_controller.setSleepTime(speed);
	}

	public void setDetailLevel(int detailLevel) {
		m_controller.setDetailLevel(detailLevel);
	}

}
